/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using DaveSexton.DocProject.Sandcastle.TopicManagement;
using System.Text.RegularExpressions;

namespace DaveSexton.DocProject.Sandcastle
{
	[Serializable]
	public sealed class ReflectionApiElement
	{
		#region Public Properties
		public string AssemblyName
		{
			get
			{
				return assembly;
			}
		}

		public string TopicId
		{
			get
			{
				return id;
			}
		}

		/// <remarks>
		/// The value of this property can be used to identify this API in the <c>apiFilter</c> of an <em>MRefBuilder.config</em> file.
		/// </remarks>
		public string TopicName
		{
			get
			{
				return name;
			}
		}

		public string Title
		{
			get
			{
				return title;
			}
		}

		/// <remarks>
		/// The value of this field is taken from the <c>file</c> element that is applied by Sandcastle doc model transformations.
		/// If this class is used to parse a <em>reflection.org</em> file generated by <em>MRefBuilder.exe</em>, without a doc model
		/// applied, then the value of this property will be <see langword="null"/>.
		/// </remarks>
		public string File
		{
			get
			{
				return file;
			}
		}

		public TopicType TopicType
		{
			get
			{
				return type;
			}
		}

		public TopicAccessibility TopicAccess
		{
			get
			{
				return access;
			}
		}

		public string ParentNamespaceId
		{
			get
			{
				return parentNamespaceId;
			}
		}

		public string ParentTypeId
		{
			get
			{
				return parentTypeId;
			}
		}

		public bool HasTemplateParameters
		{
			get
			{
				return hasTemplateParameters;
			}
		}

		public bool HasMethodParameters
		{
			get
			{
				return hasMethodParameters;
			}
		}

		public bool HasMethodOverloads
		{
			get
			{
				return hasMethodOverloads;
			}
		}

		public ICollection<string> MethodParameters
		{
			get
			{
				return methodParameters.AsReadOnly();
			}
		}

		public ICollection<string> TypeParameters
		{
			get
			{
				return typeParameters.AsReadOnly();
			}
		}

		public bool IsMethodOverloads
		{
			get
			{
				return isMethodOverloads;
			}
		}

		public bool IsApi
		{
			get
			{
				return isApi;
			}
		}

		public System.Xml.XPath.IXPathNavigable Element
		{
			get
			{
				return element;
			}
		}
		#endregion

		#region Private / Protected
		private static readonly Regex parseTopicIdRegex = new Regex(
			@"((?<NonAPI>.*?)\.)?  (?<Type>\w+) : 
				(?<Name> 
					(?<BaseName> .*? )
					(\.)? 
					(?<SimpleName> 
						\# (?<Ctor> c{1,2}tor )
						| ((?<Module>\<)?
							[^\.`]+?
							(?<Generic>`[^\#\.\(]+)?)
					)?
				) (\( .* )? $",
			RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace | RegexOptions.CultureInvariant);

		private readonly bool hasTemplateParameters, hasMethodParameters, isMethodOverloads, hasMethodOverloads, isApi;
		private readonly string id, title, name, assembly, file, parentNamespaceId, parentTypeId;
		private readonly TopicType type;
		private readonly TopicAccessibility access;
		[NonSerialized]
		private readonly XmlElement element;
		private readonly List<string> methodParameters = new List<string>(8), typeParameters = new List<string>(4);
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="ReflectionApiElement" /> class for internal use.
		/// </summary>
		public ReflectionApiElement(string documentationSetName, XmlReader reader)
		{
			if (reader == null)
				throw new ArgumentNullException("reader");

			if (!reader.IsStartElement("api"))
				throw new ArgumentException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Resources.Text.InvalidApiElementReaderPosition, Environment.NewLine, reader.ReadInnerXml()));
			else
			{
				id = reader.GetAttribute("id", "");
				isApi = true;		// default

				Match parsedId = parseTopicIdRegex.Match(id);
				Group nonApiGroup = parsedId.Groups["NonAPI"];
				Group generic = parsedId.Groups["Generic"];

				XmlDocument document = new XmlDocument();
				document.PreserveWhitespace = true;
				document.Load(reader);
				element = document.DocumentElement;

				StringBuilder titleBuilder = new StringBuilder();

				if (!string.IsNullOrEmpty(id) && id.StartsWith("R:", StringComparison.OrdinalIgnoreCase))
				{
					name = "Project";
					type = TopicType.Project;
					titleBuilder.Append(documentationSetName);
				}
				else
				{
					reader = new XmlNodeReader(element);
					reader.ReadStartElement("api");

					do
					{
						if (reader.NodeType != XmlNodeType.Element || reader.Depth > 1)
							continue;

						if (reader.IsStartElement("topicdata"))
						// This element is only available after applying a doc model transformation to the reflection.org file 
						// that is produced by MRefBuilder.exe.  The two doc models that ship with Sandcastle will both work: 
						// ApplyVSDocModel.xsl and ApplyPrototypeDocModel.xsl
						{
							if (!string.Equals(reader.GetAttribute("group", ""), "api", StringComparison.OrdinalIgnoreCase))
								isApi = false;

							if (string.Equals(reader.GetAttribute("subgroup", ""), "overload", StringComparison.OrdinalIgnoreCase))
								isMethodOverloads = true;
						}
						else if (reader.IsStartElement("apidata"))
						{
							type = GetTopicType(nonApiGroup, generic, reader, out name);

							if (string.IsNullOrEmpty(name))
								titleBuilder.Append("Default " + type.ToString());
							else if (isApi && type == TopicType.Class && generic.Success)
								titleBuilder.Append(name.Substring(0, name.IndexOf('`')));
							else
								titleBuilder.Append(name);
						}
						else if (reader.IsStartElement("typedata"))
						{
							access = GetTopicAccess(reader.GetAttribute("visibility", ""));
						}
						else if (reader.IsStartElement("memberdata"))
						{
							access = GetTopicAccess(reader.GetAttribute("visibility", ""));

							// The "overload" attribute is added by the ApplyVSDocModel.xsl and ApplyPrototypeDocModel.xsl transformations.
							if (!string.IsNullOrEmpty(reader.GetAttribute("overload", "")))
								hasMethodOverloads = true;
						}
						else if (reader.IsStartElement("templates"))
						{
							hasTemplateParameters = true;

							if (isApi)
							{
								using (XmlReader parametersReader = reader.ReadSubtree())
								{
									AppendTemplateParameterList(titleBuilder, parametersReader);
								}
							}
						}
						else if (reader.IsStartElement("parameters"))
						{
							hasMethodParameters = true;

							if (!isMethodOverloads)
							{
								using (XmlReader parametersReader = reader.ReadSubtree())
								{
									AppendMethodParameterList(titleBuilder, parametersReader);
								}
							}
						}
						else if (reader.IsStartElement("containers"))
						{
							using (XmlReader containersReader = reader.ReadSubtree())
							{
								GetContainers(containersReader, ref assembly, ref parentNamespaceId, ref parentTypeId);
							}
						}
						else if (reader.IsStartElement("file"))
						// This element is only available after applying a doc model transformation to the reflection.org file 
						// that is produced by MRefBuilder.exe.  The two doc models that ship with Sandcastle will both work: 
						// ApplyVSDocModel.xsl and ApplyPrototypeDocModel.xsl
						{
							file = reader.GetAttribute("name", "");
						}
					}
					while (reader.Read());
				}

				title = titleBuilder.ToString();
			}
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="ReflectionApiElement" /> class for internal use.
		/// </summary>
		public ReflectionApiElement(ReflectionApiElement clone)
		{
			if (clone == null)
				throw new ArgumentNullException("clone");

			isApi = clone.isApi;
			hasMethodOverloads = clone.hasMethodOverloads;
			isMethodOverloads = clone.isMethodOverloads;
			hasTemplateParameters = clone.hasTemplateParameters;
			hasMethodParameters = clone.hasMethodParameters;
			id = clone.id;
			title = clone.title;
			name = clone.name;
			assembly = clone.assembly;
			file = clone.file;
			type = clone.type;
			access = clone.access;

			if (clone.element != null)
				element = (XmlElement) clone.element.CloneNode(true);
		}
		#endregion

		#region Methods
		public static string ParseBaseName(string id)
		{
			Match match = parseTopicIdRegex.Match(id);

			if (match.Success && match.Groups["BaseName"].Success)
				return match.Groups["BaseName"].Value;
			else
				return null;
		}

		public static string ParseFullName(string id)
		{
			Match match = parseTopicIdRegex.Match(id);

			if (match.Success && match.Groups["Name"].Success)
				return match.Groups["Name"].Value;
			else
				return null;
		}

		public static string ParseSimpleName(string id)
		{
			Match match = parseTopicIdRegex.Match(id);

			if (match.Success && match.Groups["SimpleName"].Success)
				return match.Groups["SimpleName"].Value;
			else
				return null;
		}

		public static string ParseName(string id, bool simpleNameIfAvailable)
		{
			Match match = parseTopicIdRegex.Match(id);

			if (match.Success)
			{
				if (simpleNameIfAvailable && match.Groups["SimpleName"].Success)
					return match.Groups["SimpleName"].Value;
				else if (match.Groups["Name"].Success)
					return match.Groups["Name"].Value;
			}

			return null;
		}

		public static bool IsMember(TopicType type)
		{
			switch (type)
			{
				case TopicType.Event:
				case TopicType.Field:
				case TopicType.Member:
				case TopicType.Method:
				case TopicType.Property:
					return true;
				default:
					return false;
			}
		}

		public static bool IsType(TopicType type)
		{
			switch (type)
			{
				case TopicType.Type:
				case TopicType.Structure:
				case TopicType.Delegate:
				case TopicType.Enum:
				case TopicType.Class:
				case TopicType.Interface:
					return true;
				default:
					return false;
			}
		}

		public void Write(XmlWriter writer)
		{
			if (writer == null)
				throw new ArgumentNullException("writer");

			writer.WriteNode(element.CreateNavigator(), true);
		}

		private static TopicAccessibility GetTopicAccess(string visibility)
		{
			switch (visibility)
			{
				case "public":
					return TopicAccessibility.Public;
				case "family":
					return TopicAccessibility.Protected;
				case "assembly":
					return TopicAccessibility.Internal;
				case "private":
					return TopicAccessibility.Private;
				case "family or assembly":
				case "family and assembly":		// I don't think C# supports this
					return TopicAccessibility.ProtectedInternal;
				default:
					return TopicAccessibility.Public;
			}
		}

		private static TopicType GetTopicType(Group nonApiGroup, Group generic, XmlReader reader, out string name)
		{
			if (nonApiGroup.Success)
			{
				name = nonApiGroup.Value;

				if ("AllMembers".Equals(name, StringComparison.OrdinalIgnoreCase))
					name = "All Members";
			}
			else
			{
				name = reader.GetAttribute("name", "");

				switch (reader.GetAttribute("group", ""))
				{
					case "namespace":
						return TopicType.Namespace;
					case "type":
						switch (reader.GetAttribute("subgroup", ""))
						{
							case "class":
								if (name.Length > 0 && name[0] == '<')
									return TopicType.Module;
								else if (generic.Success)
									name += generic.Value;

								return TopicType.Class;
							case "structure":
								return TopicType.Structure;
							case "interface":
								return TopicType.Interface;
							case "enumeration":
								return TopicType.Enum;
							case "delegate":
								return TopicType.Delegate;
						}
						break;
					case "member":
						switch (reader.GetAttribute("subgroup", ""))
						{
							case "constructor":
							case "method":
								if (string.Equals(reader.GetAttribute("subsubgroup", ""), "operator", StringComparison.OrdinalIgnoreCase))
									name = "op_" + name;

								return TopicType.Method;
							case "property":
								return TopicType.Property;
							case "field":
								return TopicType.Field;
							case "event":
								return TopicType.Event;
						}
						break;
				}
			}

			return TopicType.General;
		}

		private static void GetContainers(XmlReader reader, ref string assembly, ref string parentNamespaceId, ref string parentTypeId)
		{
			while (reader.Read())
			{
				if (reader.Depth == 1)
				{
					if (reader.IsStartElement("library"))
						assembly = reader.GetAttribute("assembly");
					else if (reader.IsStartElement("namespace"))
						parentNamespaceId = reader.GetAttribute("api");
					else if (reader.IsStartElement("type"))
						parentTypeId = reader.GetAttribute("api");
				}
			}
		}

		/// <summary>
		/// Creates the template types list for types and methods that have generic parameters.
		/// </summary>
		private void AppendTemplateParameterList(StringBuilder titleBuilder, XmlReader reader)
		{
			titleBuilder.Append('<');

			bool hasArgs = false;

			while (reader.Read())
			{
				if (reader.IsStartElement("template") && reader.Depth == 1)
				{
					if (hasArgs)
						titleBuilder.Append(',');
					else
						hasArgs = true;

					string paramName = reader.GetAttribute("name", "");

					if (!string.IsNullOrEmpty(paramName))
					{
						titleBuilder.Append(paramName);
						typeParameters.Add(paramName);
					}
					else
						titleBuilder.Append("?");
				}
			}

			titleBuilder.Append('>');
		}

		private void AppendMethodParameterList(StringBuilder titleBuilder, XmlReader reader)
		{
			titleBuilder.Append('(');

			bool hasArgs = false;

			while (reader.Read())
			{
				if (reader.IsStartElement("parameter") && reader.Depth == 1)
				{
					if (hasArgs)
						titleBuilder.Append(", ");
					else
						hasArgs = true;

					using (XmlReader subreader = reader.ReadSubtree())
					{
						AppendMethodParameter(titleBuilder, reader);
					}
				}
			}

			titleBuilder.Append(')');
		}

		private void AppendMethodParameter(StringBuilder titleBuilder, XmlReader reader)
		{
			string paramName = reader.GetAttribute("name", "");

			methodParameters.Add(paramName);

			paramName = "{" + ((string.IsNullOrEmpty(paramName)) ? "?" : paramName) + "}";		// default if parsing fails

			string isOut = reader.GetAttribute("out", "");
			StringBuilder arraySuffix = null;

			if (reader.Read() && reader.Depth == 2)
			{
				if (reader.IsStartElement("referenceTo"))
				{
					if (string.Equals(isOut, "true", StringComparison.OrdinalIgnoreCase))
						titleBuilder.Append("out ");
					else
						titleBuilder.Append("ref ");

					reader.Read();
				}

				if (reader.IsStartElement("arrayOf"))
				{
					string rankAttribute = reader.GetAttribute("rank", "");
					int rank = 1;

					if (string.IsNullOrEmpty(rankAttribute) || int.TryParse(rankAttribute, out rank))
					{
						arraySuffix = new StringBuilder("[");

						if (rank > 0)
							arraySuffix.Append(',', rank - 1);
						else
							arraySuffix.Append('?');

						arraySuffix.Append("]");
					}
					else
						arraySuffix = new StringBuilder("[?]");

					reader.Read();
				}

				if (reader.IsStartElement("template"))
					paramName = reader.GetAttribute("name", "");
				else if (reader.IsStartElement("type"))
				{
					string id = reader.GetAttribute("api", "");
					paramName = ParseName(id, true) ?? id;
				}
			}

			if (string.IsNullOrEmpty(paramName))
				titleBuilder.Append("?");
			else
				titleBuilder.Append(paramName);

			if (arraySuffix != null)
				titleBuilder.Append(arraySuffix);
		}

		public bool IsDependentOn(ReflectionApiElement relatedApi, bool tryRemoveDependency)
		{
			if (relatedApi == null)
				throw new ArgumentNullException("relatedApi");

			if (relatedApi.type == TopicType.Namespace)
				return string.Equals(relatedApi.id, this.parentNamespaceId, StringComparison.Ordinal);
			else if (IsType(relatedApi.type))
			{
				if (this.type == TopicType.Namespace)
					return IsDependentOnElement(relatedApi.id, this.element, tryRemoveDependency);

				string parent = this.parentTypeId;
				
				if (this.type == TopicType.General)
				{
					XmlElement topicdata = this.element["topicdata"];

					if (topicdata != null)
						parent = topicdata.GetAttribute("typeTopicId");
				}

				return string.Equals(relatedApi.id, parent, StringComparison.Ordinal);
			}
			else if (IsMember(relatedApi.type))
			{
				if (this.type == TopicType.General || IsType(this.type))
					return IsDependentOnElement(relatedApi.id, this.element, tryRemoveDependency);
			}
			
			return false;
		}

		private static bool IsDependentOnElement(string id, System.Xml.XPath.IXPathNavigable api, bool tryRemoveDependency)
		{
			System.Xml.XPath.XPathNavigator navigator = api.CreateNavigator();

			if (navigator.MoveToChild("elements", ""))
			{
				foreach (System.Xml.XPath.XPathNavigator element in navigator.SelectChildren(System.Xml.XPath.XPathNodeType.Element))
				{
					if (string.Equals(id, element.GetAttribute("api", ""), StringComparison.Ordinal))
					{
						if (!tryRemoveDependency)
							return true;

						element.DeleteSelf();

						System.Xml.XPath.XPathNodeIterator children = navigator.SelectChildren(System.Xml.XPath.XPathNodeType.Element);

						// if the last local element reference was removed then the API may be excluded by simply returning true
						// indicating that the specified API is dependent on an API that was already excluded.
						// (e.g., there's no sense in documenting a namespace without types, a type without members, 
						// an overloads topic without methods, nor a properties topic without properties)
						if (children.Count == 0)
							return true;
						else
						{
							bool hasLocalElements = false;

							foreach (System.Xml.XPath.XPathNavigator child in children)
							{
								if (!child.HasChildren || child.MoveToChild("element", ""))
								{
									hasLocalElements = true;
									break;
								}
							}

							if (!hasLocalElements)
								return true;
						}

						break;
					}
				}
			}

			return false;
		}

		public override string ToString()
		{
			return title;
		}
		#endregion
	}
}
